<?php
/**
 * Class UserInterface_InputProcessor
 */
class UserInterface_InputProcessor
{
    private $allowGetArray = true;
    private $enableCsrf = true;
    private $enableXss = true;
    private $standardizeNewLines = true;

    /**
     * @var UserInterface_IInputSecurity
     */
    protected $inputSecurity;

    /**
     * @param UserInterface_IInputSecurity $inputSecurity
     */
    public function __construct(UserInterface_IInputSecurity $inputSecurity)
    {
        $this->inputSecurity = $inputSecurity;
        $this->sanitizeGlobals();
    }


    /**
     * Sanitize Globals
     *
     * Internal method serving for the following purposes:
     *
     * - Unsets $_GET data (if query strings are not enabled)
     * - Unsets all globals if register_globals is enabled
     * - Cleans POST, COOKIE and SERVER data
     * - Standardizes newline characters to PHP_EOL
     *
     * @return void
     */
    protected function sanitizeGlobals()
    {
        // It would be "wrong" to unset any of these GLOBALS.
        $protected = array(
            '_SERVER',
            '_GET',
            '_POST',
            '_FILES',
            '_REQUEST',
            '_SESSION',
            '_ENV',
            'GLOBALS',
            'HTTP_RAW_POST_DATA',
            'system_folder',
            'application_folder',
            'BM',
            'EXT',
            'CFG',
            'URI',
            'RTR',
            'OUT',
            'IN'
        );

        // Unset globals for security.
        // This is effectively the same as register_globals = off
        // PHP 5.4 no longer has the register_globals functionality.
        if (version_compare(PHP_VERSION, '5.4', '<')) {
            foreach (array($_GET, $_POST, $_COOKIE) as $global) {
                if (is_array($global)) {
                    foreach ($global as $key => $val) {
                        if (!in_array($key, $protected)) {
                            global $$key;
                            $$key = null;
                        }
                    }
                } elseif (!in_array($global, $protected)) {
                    global $$global;
                    $$global = null;
                }
            }
        }

        // Is $_GET data allowed? If not we'll set the $_GET to an empty array
        if ($this->allowGetArray === false) {
            $_GET = array();
        } elseif (is_array($_GET) and count($_GET) > 0) {
            foreach ($_GET as $key => $val) {
                $_GET[$this->cleanInputKeys($key)] = $this->cleanInputData($val);
            }
        }

        // Clean $_POST Data
        if (is_array($_POST) and count($_POST) > 0) {
            foreach ($_POST as $key => $val) {
                $_POST[$this->cleanInputKeys($key)] = $this->cleanInputData($val);
            }
        }

        // Clean $_SERVER Data
        if (is_array($_SERVER) and count($_SERVER) > 0) {
            foreach ($_SERVER as $key => $val) {
                $_SERVER[$this->cleanInputKeys($key)] = $this->cleanInputData($val);
            }
        }

        // Clean $_COOKIE Data
        if (is_array($_COOKIE) and count($_COOKIE) > 0) {
            // Also get rid of specially treated cookies that might be set by a server
            // or silly application, that are of no use to a CI application anyway
            // but that when present will trip our 'Disallowed Key Characters' alarm
            // http://www.ietf.org/rfc/rfc2109.txt
            // note that the key names below are single quoted strings, and are not PHP variables
            unset($_COOKIE['$Version']);
            unset($_COOKIE['$Path']);
            unset($_COOKIE['$Domain']);

            foreach ($_COOKIE as $key => $val) {
                $_COOKIE[$this->cleanInputKeys($key)] = $this->cleanInputData($val);
            }
        }

        // Sanitize PHP_SELF
        $_SERVER['PHP_SELF'] = strip_tags($_SERVER['PHP_SELF']);

        // CSRF Protection check
        if ($this->enableCsrf === true and !$this->isCliRequest()) {
            $this->inputSecurity->csrf_verify();
        }

        //Log::debug('Global POST and COOKIE data sanitized');
    }


    /**
     * Fetch an item from the GET array
     *
     * @param string $index Index for item to be fetched from $_GET
     * @param bool $xss_clean Whether to apply XSS filtering
     *
     * @return mixed
     */
    public function request($index = null, $xss_clean = false)
    {
        $request = $this->post($index, $xss_clean);
        if (!$request) {
            $request = $this->get($index, $xss_clean);
        }
        return $request;
    }


    /**
     * Fetch an item from the GET array
     *
     * @param string $index Index for item to be fetched from $_GET
     * @param bool $xss_clean Whether to apply XSS filtering
     *
     * @return mixed
     */
    public function get($index = null, $xss_clean = false)
    {
        // Check if a field has been provided
        if ($index === null) {
            if (empty($_GET)) {
                return array();
            }

            $get = array();

            // loop through the full _GET array
            foreach (array_keys($_GET) as $key) {
                $get[$key] = $this->fetchFromArray($_GET, $key, $xss_clean);
            }
            return $get;
        }

        return $this->fetchFromArray($_GET, $index, $xss_clean);
    }


    /**
     * Fetch an item from the POST array
     *
     * @param string $index Index for item to be fetched from $_POST
     * @param bool $xss_clean Whether to apply XSS filtering
     *
     * @return mixed
     */
    public function post($index = null, $xss_clean = false)
    {
        // Check if a field has been provided
        if ($index === null) {
            if (empty($_POST)) {
                return array();
            }

            $post = array();

            // Loop through the full _POST array and return it
            foreach (array_keys($_POST) as $key) {
                $post[$key] = $this->fetchFromArray($_POST, $key, $xss_clean);
            }
            return $post;
        }

        return $this->fetchFromArray($_POST, $index, $xss_clean);
    }


    public function inGet($index)
    {
        return array_key_exists($index, $_GET);
    }

    public function inPost($index)
    {
        return array_key_exists($index, $_POST);
    }

    public function inRequest($index)
    {
        return $this->inGet($index) or $this->inPost($index);
    }

    /**
     * Fetch an item from the SERVER array
     *
     * @param string $index Index for item to be fetched from $_POST
     * @param bool $xss_clean Whether to apply XSS filtering
     *
     * @return mixed
     */
    public function server($index = null, $xss_clean = false)
    {
        // Check if a field has been provided
        if ($index === null) {
            if (empty($_SERVER)) {
                return array();
            }

            $post = array();

            // Loop through the full _POST array and return it
            foreach (array_keys($_SERVER) as $key) {
                $post[$key] = $this->fetchFromArray($_SERVER, $key, $xss_clean);
            }
            return $post;
        }

        return $this->fetchFromArray($_SERVER, $index, $xss_clean);
    }


    /**
     * Clean Input Data
     *
     * Internal method that aids in escaping data and
     * standardizing newline characters to PHP_EOL.
     *
     * @param string|string[] $str Input string(s)
     *
     * @return string
     */
    protected function cleanInputData($str)
    {
        if (is_array($str)) {
            $new_array = array();
            foreach (array_keys($str) as $key) {
                $new_array[$this->cleanInputKeys($key)] = $this->cleanInputData($str[$key]);
            }
            return $new_array;
        }

        /**
         * We strip slashes if magic quotes is on to keep things consistent
         * NOTE: In PHP 5.4 get_magic_quotes_gpc() will always return 0 and
         * it will probably not exist in future versions at all.
         */
        if (version_compare(PHP_VERSION, '5.4', '<') and get_magic_quotes_gpc()) {
            $str = stripslashes($str);
        }

        // Remove control characters
        $str = self::removeInvisibleCharacters($str);

        // Should we filter the input data?
        if ($this->enableXss === true) {
            $str = $this->inputSecurity->xss_clean($str);
        }

        // Standardize newlines if needed
        if ($this->standardizeNewLines === true and strpos($str, "\r") !== false) {
            return str_replace(array("\r\n", "\r", "\r\n\n"), PHP_EOL, $str);
        }

        return $str;
    }


    /**
     * Clean Keys
     *
     * Internal method that helps to prevent malicious users
     * from trying to exploit keys we make sure that keys are
     * only named with alpha-numeric text and a few other items.
     *
     * @param    string $str Input string
     * @return    string
     */
    protected function cleanInputKeys($str)
    {
        if (!preg_match('/^[a-z0-9:_\/|-]+$/i', $str)) {
            header('HTTP/1.1 503 Service Temporarily Unavailable');
            exit('Disallowed Key Characters.');
        }

        return $str;
    }


    /**
     * Is CLI request?
     *
     * Test to see if a request was made from the command line.
     *
     * @return bool
     */
    public function isCliRequest()
    {
        return (php_sapi_name() === 'cli' OR defined('STDIN'));
    }


    /**
     * Fetch from array
     *
     * Internal method used to retrieve values from global arrays.
     *
     * @param  array &$array $_GET, $_POST, $_COOKIE, $_SERVER, etc.
     * @param  string $index Index for item to be fetched from $array
     * @param  bool $xss_clean Whether to apply XSS filtering
     *
     * @return mixed
     */
    protected function fetchFromArray(&$array, $index = '', $xss_clean = false)
    {
        if (!isset($array[$index])) {
            return null;
        }

        if ($xss_clean === true) {
            return $this->inputSecurity->xss_clean($array[$index]);
        }

        return $array[$index];
    }

    /**
     * Remove Invisible Characters
     *
     * This prevents sandwiching null characters
     * between ascii characters, like Java\0script.
     *
     * @param string $str The string to process
     * @param bool $url_encoded Also process URL encoded characters
     *
     * @return string
     */
    public static function removeInvisibleCharacters($str, $url_encoded = true)
    {
        $non_displayables = array();

        // every control character except newline (dec 10),
        // carriage return (dec 13) and horizontal tab (dec 09)
        if ($url_encoded) {
            $non_displayables[] = '/%0[0-8bcef]/'; // url encoded 00-08, 11, 12, 14, 15
            $non_displayables[] = '/%1[0-9a-f]/'; // url encoded 16-31
        }

        $non_displayables[] = '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S'; // 00-08, 11, 12, 14-31, 127

        do {
            $str = preg_replace($non_displayables, '', $str, -1, $count);
        } while ($count);

        return $str;
    }

}